Skip to content

Add Structlog Handler as Instrumentation#4286

Open
JWinermaSplunk wants to merge 12 commits intoopen-telemetry:mainfrom
JWinermaSplunk:structlog-handler
Open

Add Structlog Handler as Instrumentation#4286
JWinermaSplunk wants to merge 12 commits intoopen-telemetry:mainfrom
JWinermaSplunk:structlog-handler

Conversation

@JWinermaSplunk
Copy link
Copy Markdown

@JWinermaSplunk JWinermaSplunk commented Mar 3, 2026

Description

Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.

Fixes open-telemetry/opentelemetry-python#2993

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

  • test_structlog.py

Does This PR Require a Core Repo Change?

  • Yes. - Link to PR:
  • No.

Checklist:

See contributing.md for styleguide, changelog guidelines, and more.

  • Followed the style guidelines of this project
  • Changelogs have been updated
  • Unit tests have been added
  • Documentation has been updated

@JWinermaSplunk JWinermaSplunk marked this pull request as ready for review March 13, 2026 17:14
@JWinermaSplunk JWinermaSplunk requested a review from a team as a code owner March 13, 2026 17:14
@JWinermaSplunk JWinermaSplunk changed the title [WIP] Add Structlog Handler as Instrumentation Add Structlog Handler as Instrumentation Mar 13, 2026
return _STD_TO_OTEL[levelno]


class StructlogHandler:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be simplified by subclassing an existing type in structlog (which I don't know that well 🙂 )?

@tammy-baylis-swi
Copy link
Copy Markdown
Contributor

Thank you for undertaking this!

Comment thread opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap_gen.py Outdated
@xrmx xrmx moved this to Reviewed PRs that need fixes in Python PR digest Mar 25, 2026
@tammy-baylis-swi
Copy link
Copy Markdown
Contributor

I think a rebase is needed. This PR is now reverting some unrelated docker-test fixes (this other PR)

@JWinermaSplunk
Copy link
Copy Markdown
Author

I think a rebase is needed. This PR is now reverting some unrelated docker-test fixes (this other PR)

Believe I went ahead and rebased, but feel free to let me know if it still looks off!

Comment thread CHANGELOG.md Outdated
@dodofarm
Copy link
Copy Markdown

Hey team,
any timeline on this PR? This is a great addition that we've been anticipating for quite some time!

# Conflicts:
#	.github/workflows/test_0.yml
#	.github/workflows/test_1.yml
#	.github/workflows/test_2.yml
#	opentelemetry-contrib-instrumentations/pyproject.toml

# Conflicts:
#	.github/workflows/core_contrib_test.yml
# Conflicts:
#	.github/workflows/test_1.yml
#	.github/workflows/test_2.yml
#	.github/workflows/test_3.yml
Copy link
Copy Markdown
Member

@pmcollins pmcollins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Josh, this instrumentation seems to work fine, but I added some comments.

return None


class StructlogHandler:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be preferable to call this a StructlogProcessor since that's what structlog calls them? Also, below, we have processor = StructlogHandler(logger_provider=logger_provider).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, I can adjust the line below, but I went with handler instead of processor to follow the logging handler from logging instrumentation, I believe. Thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LoggingHandler name makes sense because it's a subclass of logging.Handler. In this case the class doesn't extend anything -- it's just a callable that structlog considers a processor. I don't see a need to match terminology with that library, but won't block on this if you disagree.

Comment on lines +104 to +105
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we want this because we could be classifying a local timestamp as UTC here. For example, one of the default structlog processors is TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False) which doesn't include a %z, so the emitted string doesn't include a timezone component, tzinfo is None, and we'd be then shifting the time to UTC.

Comment on lines +330 to +334
StructlogInstrumentor._processor = processor

# Wrap structlog.configure so that if user code calls it after
# instrumentation, the handler is re-inserted into the new chain.
StructlogInstrumentor._original_configure = structlog.configure
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we convert these class variables to instance variables?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be wrong, but isn't this the same behavior as existing instrumentations that use BaseInstrumentor and therefore would be protected by that singleton guard?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but I think variables should have the narrowest scope possible.

# instrumentation, the handler is re-inserted into the new chain.
StructlogInstrumentor._original_configure = structlog.configure

def _patched_configure(**kwargs):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: since this is a closure I don't think you need the leading underscore.

Comment on lines +360 to +361
if StructlogInstrumentor._processor is None:
return
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this test is necessary -- looks like the only way _processor can be None is if the user calls uninstrument without calling instrument, but seems like that's guarded against by the parent class. Which means the class variable can go away and patched_configure can just refer to _processor as a local (or just processor) since it's a closure.

)
kwargs["processors"] = processors
original = StructlogInstrumentor._original_configure
if original is not None:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can original be None here? Might be an unnecessary test.


# Get the log level and map to OTel severity
level_str = event_dict.get("level", "info")
levelno = _STRUCTLOG_LEVEL_TO_LEVELNO.get(level_str.lower(), 20)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like 20 maps to INFO -- might be more appropriate to use Otel's UNSPECIFIED level that maps to 0. You may be able to use SeverityNumber.UNSPECIFIED in the API package.

# Handle exception information
exc_info = event_dict.get("exc_info")

if exc_info is True:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to add a comment here that is True is required to prevent matching on a non-empty tuple or exception which are also truthy. (Pycharm linter is flagging this)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Reviewed PRs that need fixes

Development

Successfully merging this pull request may close these issues.

Add handler for structlog

5 participants